package com.atguigu.gmall.activity.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.activity.mapper.SeckillOrderMapper;
import com.atguigu.gmall.activity.pojo.SeckillOrder;
import com.atguigu.gmall.activity.pojo.UserRecode;
import com.atguigu.gmall.activity.service.SeckillOrderService;
import com.atguigu.gmall.activity.util.DateUtil;
import com.atguigu.gmall.activity.util.SeckillUtil;
import com.atguigu.gmall.common.execption.GmallException;
import com.atguigu.gmall.common.util.GmallThreadLocalUtils;
import com.atguigu.gmall.model.activity.SeckillGoods;
import com.atguigu.gmall.model.enums.OrderStatus;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/***
 * 秒杀订单的service层接口的实现类
 */
@Service
@Slf4j
public class SeckillOrderServiceImpl implements SeckillOrderService {

    @Resource
    private SeckillOrderMapper seckillOrderMapper;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 添加订单（排队异步下单操作）
     *
     * @param dateTime
     * @param seckillGoodsId
     * @param num
     * @return
     */
    @Override
    public UserRecode addSeckillOrder(String dateTime, String seckillGoodsId, Integer num) {
        //创建用户排队对象
        UserRecode userRecode = new UserRecode();
        //写死用户名--TODO
        String username = "jyw";

        //防止用户用户重复下单，使用redis的自增控制，记录用户排队的次数
        Long userQueueCount =
                redisTemplate.opsForValue().increment("user_queue_count_" + username, 1);
        if (userQueueCount > 1) {
            //只允许用户对单个商品下一次单
            //封装正常数据
            userRecode.setStatus(3);
            userRecode.setMsg("重复排队,排队失败!");
            //返回结果
            return userRecode;
        }

        //封装正常数据
        userRecode.setUsername(username);
        userRecode.setCreateTime(new Date());
        userRecode.setGoodsId(seckillGoodsId);
        userRecode.setTime(dateTime);
        userRecode.setNum(num);

        //异步编排
        CompletableFuture.runAsync(() -> {
            //1 保存排队对象到redis中
            redisTemplate.opsForValue().set("user_recode_" + username, userRecode);

            //2 发送消息异步下单
            rabbitTemplate.convertAndSend("seckill_order_exchange",
                    "seckill.order",
                    JSONObject.toJSONString(userRecode));

            //防止删除计数器失败,给一个过期时间 15min
            redisTemplate.expire("user_queue_count_" + username, 15, TimeUnit.MINUTES);
        }, threadPoolExecutor);

        //2 返回结果
        return userRecode;
    }

    /**
     * 获取用户排队信息
     *
     * @return
     */
    @Override
    public UserRecode getUserRecode() {
        //先写死用户名--TODO--后期加入登录再改
        String username = "jyw";

        //1 从redis中获取
        UserRecode o = (UserRecode) redisTemplate.opsForValue().get("user_recode_" + username);

        //2 返回结果
        return o;
    }

    /**
     * 秒杀真实下单
     *
     * @param userRecodeJsonString
     */
    @Override
    public void realAddSeckillOrder(String userRecodeJsonString) {
        //1 反序列化
        UserRecode userRecode = JSONObject.parseObject(userRecodeJsonString, UserRecode.class);
        //获取用户名
        String username = userRecode.getUsername();

        //2 排除参数错误的情况
        //2.1 商品是否在活动时间中
        //获取商品所在时间段
        String time = userRecode.getTime();//2023011308
        //获取当前的时间段
        String currentDateTime = DateUtil.data2str(DateUtil.getDateMenus().get(0), DateUtil.PATTERN_YYYYMMDDHH);
        if (!currentDateTime.equals(time)) {
            //不在活动时间段内，下单失败
            this.addSeckillOrderFail(userRecode, "商品不在活动时间内");
            //结束方法
            return;
        }

        //2.2 商品是否存在
        //获取秒杀商品id
        String goodsId = userRecode.getGoodsId();
        //从redis中获取数据
        SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.opsForHash().get(time, goodsId);
        if (seckillGoods == null) {
            //商品不存在，下单失败
            this.addSeckillOrderFail(userRecode, "秒杀商品不存在");
            //结束方法
            return;
        }

        //2.3 购买数量是否非法
        //获取购买的数量
        Integer num = userRecode.getNum();
        if (num <= 0) {
            //购买的数量为负
            this.addSeckillOrderFail(userRecode, "购买的商品的数量非法，限购数量为：" + num);
            //结束方法
            return;
        }

        //2.4 购买数量是否超过限购数量
        //获取秒杀商品限购的数量
        Integer seckillLimit = seckillGoods.getSeckillLimit();
        if (num > seckillLimit) {
            //超过限购数量，下单失败
            this.addSeckillOrderFail(userRecode, "超过限购数量");
            //结束方法
            return;
        }

        //2.5 购买的数量超过库存剩余数量（用户可以买多个商品）
        /**
         * 方案一：循环从库存队列里取值，
         * 存在 a.相对公平问题（库存为2，A、B、C先后下单，A卖3库存不足，则需要回滚，在A还未完成回滚之前，此时B下单购买1，此时从库存队列取出的值为空，B将下单失败。C紧跟着下单，此时A已经回滚完库存，C能从库存队列中取到值，C下单成功）
         *     b.对库存队列操作可能过于频繁
         */
        for (Integer i = 0; i < num; i++) {
            Object o = redisTemplate.opsForList().rightPop("seckill_goods_stock_count_queue_" + goodsId);
            if (o == null) {
                //库存不足
                this.addSeckillOrderFail(userRecode, "购买的商品超过库存剩余数量，库存不足");
                //排除第一次循环库存中就没有数据，所以要i > 0
                if (i > 0) {
                    //库存必须大于用户购买的数量（库存3，用户买4），否则进行库存回滚（取出的库存为i）
                    redisTemplate.opsForList().leftPushAll("seckill_goods_stock_count_queue_" + SeckillUtil.getseckillGoodsIdArray(i, goodsId));
                }
                //结束方法
                return;
            }
        }

//      //原始方案：存在库存超卖问题
//      //获取秒杀商品的剩余库存
//        Integer stockCount = seckillGoods.getStockCount();
//        if(num > stockCount){
//            //超过库存剩余数量
//            this.addSeckillOrderFail(userRecode,"购买的商品超过库存剩余数量，剩余数量为：" + stockCount);
//            //结束方法
//            return;
//        }

        //3 扣减库存（排除错误情况后）
        Long stockCount = redisTemplate.opsForHash().increment("seckill_goods_stock_count_" + time,
                goodsId,
                -num);

        //更新秒杀商品的库存
        seckillGoods.setStockCount(stockCount.intValue());
        //更新redis中的秒杀商品数据
        /**
         * 注意会出现极限值：
         * 如在59分59秒下的单，走到这行代码时已经到整点，key被删除，此时商品会重写到redis中，
         * 因此需要再进行兜底清除（在活动结束后30分钟，同步完商品库存后，再删除）
         */
        redisTemplate.opsForHash().put(time, goodsId, seckillGoods);

        //4 保存秒杀订单对象
        SeckillOrder seckillOrder = new SeckillOrder();
        //封装数据
        seckillOrder.setId(UUID.randomUUID().toString().replace("-", ""));
        seckillOrder.setGoodsId(goodsId);
        seckillOrder.setMoney(seckillGoods.getPrice().multiply(new BigDecimal(num)));
        seckillOrder.setUserId(username);
        seckillOrder.setCreateTime(new Date());
        seckillOrder.setStatus(OrderStatus.UNPAID.getComment());
        seckillOrder.setNum(num);
        //保存到redis中，等到用户取消订单或支付订单才写入MySQL中
        redisTemplate.opsForHash().put("seckill_order_" + username, seckillOrder.getId(), seckillOrder);

        //5 更新用户的排队信息，正常情况
        userRecode.setStatus(2);
        userRecode.setMsg("秒杀商品下单成功！！！请立即支付");
        userRecode.setOrderId(seckillOrder.getId());
        userRecode.setMoney(seckillOrder.getMoney().doubleValue() + "");
        //更新redis中的数据
        redisTemplate.opsForValue().set("user_recode_" + username, userRecode);

        //6 下单成功发送延时消息mq，处理超时订单
        rabbitTemplate.convertAndSend(
                "seckill_order_delay_exchange",
                "seckill.order.delay",
                username,
                message -> {
                    MessageProperties properties = message.getMessageProperties();
                    properties.setExpiration(900000 + "");
                    return message;
                });
    }

    /**
     * 下单失败的处理
     *
     * @param userRecode
     * @param message
     */
    private void addSeckillOrderFail(UserRecode userRecode, String message) {
        //获取用户名
        String username = userRecode.getUsername();
        //更新用户排队状态
        userRecode.setStatus(3);
        userRecode.setMsg(message + "，秒杀下单失败！！！");
        redisTemplate.opsForValue().set("user_recode_" + username, userRecode);
        //删除控制用户重复下单的key（排队计数器）
        redisTemplate.delete("user_queue_count_" + username);
    }

    /**
     * 取消正常订单 / 取消超时订单
     *
     * @param username
     */
    @Override
    public void cancelOrder(String username) {
//        System.out.println("username = " + username);
        /**
         * 如何判断用户是超时取消还是主动取消？
         *      传入的username是否为空
         *      空：主动取消
         *      非空：超时取消
         */
        String msg = "";

        //1 判断用户名
        if (StringUtils.isEmpty(username)) {
            //主动取消 TODO--测试代码
//            username = GmallThreadLocalUtils.get();
            username = "jyw";
            msg = OrderStatus.CANCEL.getComment();
        } else {
            //超时取消
            msg = OrderStatus.TIMEOUT_CANCEL.getComment();
        }

        //2 获取用户的排队信息，拿到订单
        UserRecode userRecode = (UserRecode) redisTemplate.opsForValue().get("user_recode_" + username);
        String orderId = userRecode.getOrderId();

        /**
         * 此方法有并发，需要加锁，
         * 又因避免支付订单与取消订单操作同时进行，
         * 则使用的锁需和支付操作用同一把("seckill_order_pay_or_cancel_lock_" + orderId)
         */
        //获取锁
        RLock lock = redissonClient.getLock("seckill_order_pay_or_cancel_lock_" + orderId);
        try {
            if (lock.tryLock()) {
                //加锁成功
                try {
                    //3 获取订单数据，修改订单状态
                    SeckillOrder seckillOrder =
                            (SeckillOrder) redisTemplate.opsForHash().get("seckill_order_" + username, orderId);
                    if (seckillOrder == null) {
                        return;
                    }
                    seckillOrder.setStatus(msg);

                    //4 将订单写入数据库（注意：此时是insert操作，不同与普通订单）
                    int insert = seckillOrderMapper.insert(seckillOrder);
                    if (insert <= 0) {
                        //写入失败
                        throw new GmallException(msg + "订单时，订单写入MySQL失败", null);
                    }

                    //5 库存回滚
                    this.rollbackStockCount(seckillOrder, userRecode.getTime());

                    //6 清理redis中的标识位：用户的订单、排队信息、排队次数
                    this.clearFlagInfo(username, orderId);
                } catch (Exception e) {
                    log.error("取消订单加锁成功，但逻辑出现异常。订单号为：" + orderId + "，用户名为：" + username + "，失败的原因为：" + e.getMessage());
                    throw e;
                } finally {
                    lock.unlock();
                }
            }
        } catch (Exception e) {
            log.error("取消订单加锁失败，用户名为：" + username + "，订单号为：" + orderId + "，失败的原因为：" + e.getMessage());
            throw e;
        }
    }

    /**
     * 清理redis中的标识位：用户的订单、排队信息、排队次数
     *
     * @param username
     */
    private void clearFlagInfo(String username, String orderId) {
        //删除用户的订单
        redisTemplate.opsForHash().delete("seckill_order_" + username, orderId);
        //删除用户的排队信息
        redisTemplate.delete("user_recode_" + username);
        //删除用户的排队次数
        redisTemplate.delete("user_queue_count_" + username);
    }

    /**
     * 取消订单库存回滚
     *
     * @param seckillOrder
     * @param time
     */
    private void rollbackStockCount(SeckillOrder seckillOrder, String time) {
        //获取商品id、商品购买的数量
        String goodsId = seckillOrder.getGoodsId();
        Integer num = seckillOrder.getNum();

        //1 先获取当前时间段的指定商品id的商品剩余库存数据，并回滚，拿到回滚之后的剩余库存值
        Long stockCount =
                redisTemplate.opsForHash().increment("seckill_goods_stock_count_" + time, goodsId, num);

        //2 获取当前时间段的指定商品id的商品数据，并回滚库存，再更新redis中的值
        SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.opsForHash().get(time, goodsId);
        if (seckillGoods == null) {
            //商品不存在，说明不在活动时间内，直接结束方法
            return;
        }
        seckillGoods.setStockCount(stockCount.intValue());
        redisTemplate.opsForHash().put(time, goodsId, seckillGoods);

        //3 获取活动中的商品剩余库存的队列数据，并回滚
        redisTemplate
                .opsForList()
                .leftPushAll(
                        "seckill_goods_stock_count_queue_" + goodsId,
                        SeckillUtil.getseckillGoodsIdArray(num, goodsId));

    }

    /**
     * 订单支付成功，修改订单状态
     *
     * @param payNotifyResultJsonString
     */
    @Override
    public void updateSeckillOrder(String payNotifyResultJsonString) {
//        System.out.println("payNotifyResultJsonString = " + payNotifyResultJsonString);

        //1 反序列化
        Map<String, String> payNotifyResultMap = JSONObject.parseObject(payNotifyResultJsonString, Map.class);

        //2 获取订单id
        String orderId = payNotifyResultMap.get("out_trade_no");

        //3 获取支付通知参数中的attach参数数据，反序列化之后，拿到username
        String attachJsonString = payNotifyResultMap.get("attach");
        Map<String, String> attachMap = JSONObject.parseObject(attachJsonString, Map.class);
        String username = attachMap.get("username");

        /**
         * 此方法有并发，需要加锁，
         * 又因避免支付订单与取消订单操作同时进行，
         * 则使用的锁需和支付操作用同一把("seckill_order_pay_or_cancel_lock_" + orderId)
         */
        //获取锁
        RLock lock = redissonClient.getLock("seckill_order_pay_or_cancel_lock_" + orderId);
        try {
            if (lock.tryLock()) {
                //加锁成功
                try {
                    //4 获取redis中的秒杀订单数据, 修改订单信息
                    SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForHash().get("seckill_order_" + username, orderId);
                    if (seckillOrder == null) {
                        //订单不存在，直接结束
                        return;
                    }
                    seckillOrder.setStatus("已支付");
                    seckillOrder.setPayTime(new Date());
                    seckillOrder.setTransactionId(payNotifyResultMap.get("transaction_id"));

                    //6 将订单数据写入MySQL
                    int insert = seckillOrderMapper.insert(seckillOrder);
                    if (insert <= 0) {
                        //写入失败
                        throw new GmallException("支付成功后，订单写入数据库失败", null);
                    }

                    //7 清理redis中的标识位：用户的订单、排队信息、排队次数
                    this.clearFlagInfo(username, orderId);
                } catch (Exception e) {
                    log.error("支付成功修改订单方法，加锁成功，但方法逻辑出现异常。秒杀订单号为：" + orderId + "，用户名为：" + username + "，失败的原因为：" + e.getMessage());
                } finally {
                    lock.unlock();
                }
            }
        } catch (Exception e) {
            log.error("支付成功修改订单方法，加锁失败，秒杀订单号为：" + orderId + "，用户名为：" + username + "，失败的原因为：" + e.getMessage());
        }
    }
}